﻿using System.Net;
using System;
using System.Collections.Generic;

namespace RedisDotNet
{
    /// <summary>
    /// Redis high level client
    /// </summary>
    public class RedisClient : IRedisClient
    {
        #region Constants

        public const int DefaultPort = 6379;

        private const string StatusOk = "OK";

        #endregion

        #region Fields

        private readonly IRedisNetworkClient client;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="RedisClient"/> class.
        /// </summary>
        public RedisClient()
        {
            var ipAddress = IPAddress.Parse("127.0.0.1");
            if (ipAddress == null)
                throw new InvalidOperationException("Invalid local address");

            client = new RedisNetworkClient(ipAddress, DefaultPort);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedisClient"/> class.
        /// </summary>
        /// <param name="ipAddress">The ip address.</param>
        /// <param name="port">The port.</param>
        public RedisClient(IPAddress ipAddress, int port)
        {
            client = new RedisNetworkClient(ipAddress, port);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedisClient"/> class.
        /// </summary>
        /// <param name="networkClient">The network client.</param>
        public RedisClient(IRedisNetworkClient networkClient)
        {
            client = networkClient;
        }

        #endregion

        #region IRedisClient Members

        /// <summary>
        /// Determines if the specified key exists
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>Whether or not the key exists</returns>
        public bool Exists(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("EXISTS", key);
            return client.ExpectIntegerReply() != 0;
        }

        /// <summary>
        /// Gets the specified key as a string
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>The specified key as a string</returns>
        public string Get(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("GET", key);
            return client.ExpectBulkReply();
        }

        /// <summary>
        /// Sets the specified key to a string value
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <returns>True if set was successful</returns>
        public bool Set(string key, string value)
        {
            Check.IsValidKey(key);
            client.IssueBulkCommand("SET", key, value);
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Deletes the specified keys.
        /// </summary>
        /// <param name="keys">The keys.</param>
        /// <returns>The number of keys deleted</returns>
        public int Delete(params string[] keys)
        {
            Check.IsInRange(keys.Length, 1, Int32.MaxValue, "keys");
            Check.IsValidKey(keys);
            client.IssueSimpleCommand("DEL", string.Join(" ", keys));
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Increment key by one
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>Value after increment operation.</returns>
        public int Incr(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("INCR", key);
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Increment key by specified value
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="by">The amount to increment the key by.</param>
        /// <returns>Value after increment operation.</returns>
        public int Incr(string key, int by)
        {
            Check.IsValidKey(key);
            Check.IsInRange(by, 1, Int32.MaxValue, "by");
            client.IssueSimpleCommand("INCRBY", key, by.ToString());
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Decrement key by one
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>Value after decrement operation.</returns>
        public int Decr(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("DECR", key);
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Decrement key by one
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="by">The amount to decrement the key by.</param>
        /// <returns>Value after decrement operation.</returns>
        public int Decr(string key, int by)
        {
            Check.IsValidKey(key);
            Check.IsInRange(by, 1, Int32.MaxValue, "by");
            client.IssueSimpleCommand("DECRBY", key, by.ToString());
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Determines the type of the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public RedisType Type(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("TYPE", key);
            switch (client.ExpectSingleLineReply().ToLower())
            {
                case "none":
                    return RedisType.None;
                case "string":
                    return RedisType.String;
                case "list":
                    return RedisType.List;
                case "set":
                    return RedisType.Set;
                default:
                    throw new RedisClientException(Strings.ExUnknownType);
            }
        }

        /// <summary>
        /// Save the whole data set to disk including keys due to expire.  This
        /// blocks the processes until complete so that no other requests will
        /// be handled.
        /// </summary>
        /// <returns>True if successful</returns>
        public bool Save()
        {
            client.IssueSimpleCommand("SAVE");
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Rename a key
        /// </summary>
        /// <param name="originalKey">Original key name.</param>
        /// <param name="newKey">New key name.</param>
        /// <returns>True if successful</returns>
        public bool Rename(string originalKey, string newKey)
        {
            Check.IsValidKey(originalKey);
            Check.IsValidKey(newKey);

            client.IssueSimpleCommand("RENAME", originalKey, newKey);
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Rename a key safely, failing if the key already exists.
        /// </summary>
        /// <param name="originalKey">Original key name.</param>
        /// <param name="newKey">New key name.</param>
        /// <param name="failIfAlreadyExists">If true, fail if the new
        /// key already exists.</param>
        /// <returns>
        /// True if successful, False if already exists/orignal key
        /// didn't exist.
        /// </returns>
        public bool Rename(string originalKey, string newKey, bool failIfAlreadyExists)
        {
            Check.IsValidKey(originalKey);
            Check.IsValidKey(newKey);
            
            if (!failIfAlreadyExists)
                return Rename(originalKey, newKey);

            client.IssueSimpleCommand("RENAMENX", originalKey, newKey);
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Get count of keys
        /// </summary>
        /// <returns>Total number of keys in the database</returns>
        public int DbSize()
        {
            client.IssueSimpleCommand("DBSIZE");
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Delete all keys in current database
        /// </summary>
        /// <returns>True if successful</returns>
        public bool FlushDb()
        {
            client.IssueSimpleCommand("FLUSHDB");
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Delete all keys in all databases
        /// </summary>
        /// <returns>True if successful</returns>
        public bool FlushAll()
        {
            client.IssueSimpleCommand("FLUSHALL");
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Get the time to live for the key in seconds
        /// </summary>
        /// <param name="key">They key.</param>
        /// <returns>
        /// Time in seconds, or -1 if key does not exist or doesn't
        /// have an expiry set
        /// </returns>
        public int TimeToLive(string key)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("TTL", key);
            return client.ExpectIntegerReply();
        }

        /// <summary>
        /// Expire the key after a set time period in seconds
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="seconds">Time in seconds to expire</param>
        /// <returns>True if expiry request successful.</returns>
        public bool Expire(string key, int seconds)
        {
            Check.IsValidKey(key);
            client.IssueSimpleCommand("EXPIRE", key, seconds.ToString());
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Expire the key at a set time
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="at">Time at which to expire.</param>
        /// <returns>True if expiry request successful.</returns>
        public bool Expire(string key, DateTime at)
        {
            Check.IsValidKey(key);
            var unixTimeStamp = at.ToUnixTimeStamp().ToString();
            client.IssueSimpleCommand("EXPIREAT", key, unixTimeStamp);
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Return a random key from the server.
        /// </summary>
        /// <returns>
        /// A random key from the server or empty string if there are
        /// no keys
        /// </returns>
        public string RandomKey()
        {
            client.IssueSimpleCommand("RANDOMKEY");
            return client.ExpectSingleLineReply();
        }

        /// <summary>
        /// Query key list with pattern.
        /// </summary>
        /// <param name="pattern">Query pattern</param>
        /// <returns>List of keys</returns>
        public string[] Keys(string pattern)
        {
            client.IssueSimpleCommand("KEYS", pattern);
            var keyData = client.ExpectBulkReply();
            return keyData.Split(new[] { ' ' });
        }

        /// <summary>
        /// Selects the specified data base.
        /// </summary>
        /// <param name="dataBase">The data base.</param>
        /// <returns></returns>
        public bool Select(int dataBase)
        {
            Check.IsInRange(dataBase, 0, Int32.MaxValue, "dataBase");
            client.IssueSimpleCommand("SELECT", dataBase.ToString());
            return client.ExpectSingleLineReply() == StatusOk;
        }

        /// <summary>
        /// Move a key to a different database.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="targetDatabase">Target database index.</param>
        /// <returns></returns>
        public bool Move(string key, int targetDatabase)
        {
            Check.IsValidKey(key);
            Check.IsInRange(targetDatabase, 0, Int32.MaxValue, "targetDatabase");
            client.IssueSimpleCommand("MOVE", key, targetDatabase.ToString());
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Disconnect from server.
        /// </summary>
        public void Quit()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Authenticate connection
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>True if authentication successful.</returns>
        public bool Authenticate(string password)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Sets the specified key to a string value, failing if it already
        /// exists when failIfAlreadyExists is set.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <param name="failIfAlreadyExists">If true, fail if already exists.</param>
        /// <returns>True if successful.</returns>
        public bool Set(string key, string value, bool failIfAlreadyExists)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Set multiple keys and values.  Either completely succeeds or fails.
        /// </summary>
        /// <param name="data">Key value pair list</param>
        /// <returns>
        /// True if all keys set.  False if no key was set.
        /// </returns>
        public bool Set(List<KeyValuePair<string, string>> data)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Set multiple keys and values.  Either completely succeeds or fails.
        /// </summary>
        /// <param name="data">Key value pair list</param>
        /// <param name="failIfAlreadyExists">Fail if any key exist.</param>
        /// <returns>
        /// True if all keys set.  False if no key was set.
        /// </returns>
        public bool Set(List<KeyValuePair<string, string>> data, bool failIfAlreadyExists)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Get a key and set it to a new value in one transaction, returning
        /// the old key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <returns>The old value.</returns>
        public string GetSet(string key, string value)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Get multiple keys
        /// </summary>
        /// <param name="keys">The keys.</param>
        /// <returns>Array of returned keys.</returns>
        public string[] Get(string[] keys)
        {
            client.IssueSimpleCommand("MGET", keys);
            return client.ExpectMultiBulkReply();
        }

        #region Set operations

        /// <summary>
        /// Add member to set defined by key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="member">The member.</param>
        /// <returns>True if successful</returns>
        public bool SetAdd(string key, string member)
        {
            client.IssueBulkCommand("SADD", key, member);
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Return whether or not the specified member is in the set
        /// defined by key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="member">The member.</param>
        /// <returns>True if is member of set.</returns>
        public bool SetIsMember(string key, string member)
        {
            client.IssueBulkCommand("SISMEMBER", key, member);
            return client.ExpectIntegerReply() == 1;
        }

        /// <summary>
        /// Remove a member from set defined by key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="member">The member.</param>
        /// <returns>True if removed, False if didn't exist.</returns>
        public bool SetRemove(string key, string member)
        {
            client.IssueBulkCommand("SREM", key, member);
            return client.ExpectIntegerReply() == 1;
        }

        #endregion

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, 
        /// releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            client.Dispose();
        }

        #endregion
    }
}
